import * as THREE from '../../../../modules/three/three.module.js'
import {
  canvasCoordToScreenCoord,
  getMouseRaycaster,
  getOldToNewAngle,
} from '../util/StaticMethods.js';
import AxisArrow from './subset/AxisArrow.js';
import HitTestPlane from './subset/HitTestPlane.js';

const AXES = {
  X: 'X',
  X_: '-X',
  Y: 'Y',
  Y_: '-Y',
  Z: 'Z',
  Z_: '-Z',
  NONE: 'none',
};

const _raycaster = new THREE.Raycaster();
const _mvPosition = new THREE.Vector3();
const _modelViewMatrix = new THREE.Matrix4();
const _modelMatrix = new THREE.Matrix4();
const unitX = new THREE.Vector3(1, 0, 0);
const unitY = new THREE.Vector3(0, 1, 0);
const unitZ = new THREE.Vector3(0, 0, 1);
const sizeVector = new THREE.Vector3(1, 1, 1);
const tmpVector = new THREE.Vector3(1, 1, 1);
const vector2 = new THREE.Vector2();
const normalOnPlane = new THREE.Vector2(0, 1);
// 用于把scene空间的坐标变换到 this.object 的本地坐标空间
const objRotateInvertMatrix = new THREE.Matrix4();
// objRotateInvertMatrix 的逆矩阵
const objRotateMatrix = new THREE.Matrix4();
const tmpPosition = new THREE.Vector3();
// 只在ScaleObject3dControl.attach()方法里修改，经过镜像变换后，模型的scale的x y z 中的某个属性有可能是负数
const signVector = new THREE.Vector3(1, 1, 1);
const scaleVector = new THREE.Vector3(1, 1, 1);
const origin = new THREE.Vector3(0, 0, 0);
let scalingOnLine;
let lineX, lineY, lineZ;
const minScaleVal = 0.01; // 最小缩放比例
const cubeCenter = new THREE.Vector3();
const cubeFaceCenters = [
  new THREE.Vector3(),
  new THREE.Vector3(),
  new THREE.Vector3(),
  new THREE.Vector3(),
  new THREE.Vector3(),
  new THREE.Vector3(),
];

export const eventScaleChanged = { type: 'scale-changed', value: false };

function intersectObjectWithRay(object, raycaster, includeInvisible = false) {
  const allIntersections = raycaster.intersectObject(object, true);
  for (let i = 0; i < allIntersections.length; i++) {
    if (allIntersections[i].object.visible || includeInvisible) {
      return allIntersections[i];
    }
  }
  return false;
}

/**
 * 获取对象的原始尺寸，位于scene空间
 * 注意求对象的原始尺寸，必须遵循特定的流程，不能直接把object从场景scene中移除，然后求其原始尺寸
 * object和scene之间有若干个层级, object.parent.parent.parent... === scene;
 * 1. 重置object的旋转和缩放，使得object相对于scene是零旋转和零缩放的
 * 2. 计算object的包围盒相对于scene 的 原始尺寸
 * 例如：背板尺寸从后端拉下来尺寸是{x: 860, y: 3, z: 660}, 此函数返回的背板原始尺寸为{x: 860, y: 3, z: 660}
 * @param {THREE.Object3D} object
 * @param {THREE.Quaternion} parentToSceneQuat object.parent相对于scene的旋转
 * 比如 object.parent.parent.parent === scene,
 * objParentToSceneQuat === object.parent.quaternion.premultiply(object.parent.parent.quaternion)
 * @param {THREE.Vector3} size 输出, 相对于scene空间
 * @return {THREE.Vector3}
 */
function getObject3dSize(object, parentToSceneQuat, size) {
  const _position = new THREE.Vector3();
  const _scale = new THREE.Vector3();
  const _rotation = new THREE.Euler();
  const box3 = new THREE.Box3();
  _position.copy(object.position);
  _rotation.copy(object.rotation);
  _scale.copy(object.scale);
  object.position.set(0, 0, 0);
  // object.rotation.set(0, 0, 0)
  // euler用于重置若干个父级赋予object的旋转，使object相对于scene是零旋转的
  const euler = new THREE.Euler().setFromQuaternion(
    parentToSceneQuat.clone().invert(),
  );
  object.rotation.copy(euler);
  object.scale.set(1, 1, 1);
  object.updateMatrixWorld(true);

  box3.setFromObject(object);

  object.position.copy(_position);
  object.rotation.copy(_rotation);
  object.scale.copy(_scale);
  object.updateMatrixWorld(true);
  box3.getSize(size);
  return size;
}

/**
 * intersectPoint在直线line上的投影点pointOnLine，寻找pointOnLine离worldBox六个面的中心，哪个点的距离最近
 * @param {THREE.Line3} line，worldPosition的投影轴线，位于零变换空间
 * @param {THREE.Vector3} worldPosition 零变换空间
 * @param {THREE.Box3} worldBox 零变换空间
 * @param {THREE.Vector3} anchorVector 输出 零变换空间坐标
 */
function getScaleAnchorVector(line, worldPosition, worldBox, anchorVector) {
  // pointOnLine位于零变换空间，intersectPoint在直线line上的投影点
  const pointOnLine = line.closestPointToPoint(
    worldPosition,
    false,
    new THREE.Vector3(),
  );
  const { min, max } = worldBox;
  const center = cubeCenter.copy(min).add(max).multiplyScalar(0.5);
  cubeFaceCenters.forEach((it) => it.copy(center));
  // 上 下 左 右 前 后
  const [p0, p1, p2, p3, p4, p5] = cubeFaceCenters;
  const oppoMap = [1, 0, 3, 2, 5, 4];
  p0.y = max.y;
  p1.y = min.y;
  p2.x = min.x;
  p3.x = max.x;
  p4.z = max.z;
  p5.z = min.z;
  let minDistance = Infinity;
  cubeFaceCenters.forEach((vector, index) => {
    const distance = pointOnLine.manhattanDistanceTo(vector);
    if (minDistance > distance) {
      minDistance = distance;
      anchorVector.copy(cubeFaceCenters[oppoMap[index]]);
    }
  });
}

function flipLine3(line3) {
  tmpVector.copy(line3.start);
  line3.start.copy(line3.end);
  line3.end.copy(tmpVector);
}

function initSign(objScale) {
  signVector.x = objScale.x >= 0 ? 1 : -1;
  signVector.y = objScale.y >= 0 ? 1 : -1;
  signVector.z = objScale.z >= 0 ? 1 : -1;
  scaleVector.copy(objScale).multiply(signVector);
}

function initProjectionLines(matrixWorld) {
  lineX = new THREE.Line3(
    new THREE.Vector3(),
    unitX.clone().multiplyScalar(1000),
  ).applyMatrix4(matrixWorld);
  lineY = new THREE.Line3(
    new THREE.Vector3(),
    unitY.clone().multiplyScalar(1000),
  ).applyMatrix4(matrixWorld);
  lineZ = new THREE.Line3(
    new THREE.Vector3(),
    unitZ.clone().multiplyScalar(1000),
  ).applyMatrix4(matrixWorld);
  if (signVector.x < 0) {
    flipLine3(lineX);
  }
  if (signVector.y < 0) {
    flipLine3(lineY);
  }
  if (signVector.z < 0) {
    flipLine3(lineZ);
  }
}

export default class ScaleControl extends THREE.Object3D {
  _objParentInvMat = new THREE.Matrix4();
  _cameraLookAt = new THREE.Vector3();
  /**
   * 可以被射线撞击的网格
   * @type {THREE.Mesh[]}
   * @private
   */
  handlers = [];

  constructor(camera, domEle) {
    super();
    this.domElement = domEle || document;
    // this.domElement.style.touchAction = 'none'; // disable touch scroll

    this.visible = false;
    this.camera = camera;
    const colorX = '#ff6a00';
    const colorY = '#00ffb2';
    const colorZ = '#174dc5';

    const axisLen = 0.5;
    const axesHelper = new THREE.AxesHelper(axisLen);
    const axesColor = '#fff';
    axesHelper.setColors(axesColor, axesColor, axesColor);
    axesHelper.material.depthTest = false;
    axesHelper.renderOrder = 99;
    this.add(axesHelper);

    this._planeXY = new HitTestPlane(colorZ);
    this._planeXY.userData.normal = unitZ;
    this.add(this._planeXY);
    this._planeXZ = new HitTestPlane(colorY);
    this._planeXZ.userData.normal = unitY;
    this.add(this._planeXZ);
    this._planeXZ.rotation.x = Math.PI * 0.5;
    this._planeYZ = new HitTestPlane(colorX);
    this._planeYZ.userData.normal = unitX;
    this.add(this._planeYZ);
    this._planeYZ.rotation.y = Math.PI * 0.5;

    const arrowSize = 0.4;
    const arrowX = new AxisArrow(colorX, arrowSize, true);
    const arrowXMinus = new AxisArrow(colorX, arrowSize, true);
    const arrowY = new AxisArrow(colorY, arrowSize, true);
    const arrowYMinus = new AxisArrow(colorY, arrowSize, true);
    const arrowZ = new AxisArrow(colorZ, arrowSize, true);
    const arrowZMinus = new AxisArrow(colorZ, arrowSize, true);

    arrowX.rotation.z = Math.PI * -0.5;
    this._arrowX = arrowX;
    arrowXMinus.rotation.z = Math.PI * 0.5;
    this._arrowXMinus = arrowXMinus;

    this._arrowY = arrowY;
    arrowYMinus.rotation.z = Math.PI;
    this._arrowYMinus = arrowYMinus;

    arrowZ.rotation.x = Math.PI * 0.5;
    this._arrowZ = arrowZ;
    arrowZMinus.rotation.x = -Math.PI * 0.5;
    this._arrowZMinus = arrowZMinus;

    arrowX.name = AXES.X;
    arrowXMinus.name = AXES.X_;
    arrowY.name = AXES.Y;
    arrowYMinus.name = AXES.Y_;
    arrowZ.name = AXES.Z;
    arrowZMinus.name = AXES.Z_;
    this.arrows = [
      arrowX,
      arrowXMinus,
      arrowY,
      arrowYMinus,
      arrowZ,
      arrowZMinus,
    ];
    this.arrows.forEach((item) => {
      this.add(item);
      item.updateMatrix();
      item.children[0].geometry.applyMatrix4(item.matrix);
      item.children[0].name = item.name;
      item.rotation.set(0, 0, 0);
      this.handlers.push(item.children[0]);
    });

    this.cameraPosition = new THREE.Vector3();
    this.eye = new THREE.Vector3();
    this.dragging = false;
    this.axis = AXES.NONE;
    // this.object.parent空间中的坐标，不一定是scene空间的
    this._positionStart = new THREE.Vector3();
    // 零变换空间的坐标
    this._dragStart = new THREE.Vector3();
    // 零变换空间的坐标，位于物体的本地坐标架上，this._dragStart投影到或x，或y，或z轴上
    this._lineStart = new THREE.Vector3();
    // 零变换空间的坐标，位于物体的本地坐标架上的或x，或y，或z轴上；正在拖动的撞击点投影到本地坐标架上，就是this._lineEnd
    this._lineEnd = new THREE.Vector3();
    // 零变换空间的坐标，onDragStart中初始化，保存正在拖动轴的对向包围盒面的中心位置; 用于拉伸吸附
    this._scaleAnchor = new THREE.Vector3();
    this._tStart = 0;
    this._tEnd = 0;
    // 始终保存对象的原始尺寸
    this._objSize = new THREE.Vector3(1, 1, 1);
    // 保存对象的原始长度（或长，或宽，或高）的一半
    this._halfLength = 1;
    // 保存对象的经过拉伸后的长度（或长，或宽，或高）的一半
    this._halfScaledLength = 1;

    this._onDragStart = onDragStart.bind(this);
    this._onDragging = onDragging.bind(this);
    this._onDragEnd = onDragEnd.bind(this);
    this.domElement.addEventListener('pointerdown', this._onDragStart);
    this.domElement.addEventListener('pointermove', this._onDragging);
    this.domElement.addEventListener('pointerup', this._onDragEnd);
  }

  dispose() {
    this.domElement.removeEventListener('pointerdown', this._onDragStart);
    this.domElement.removeEventListener('pointermove', this._onDragging);
    this.domElement.removeEventListener('pointerup', this._onDragEnd);
    this.traverse((child) => {
      if (child.geometry) child.geometry.dispose();
      if (child.material) child.material.dispose();
    });
  }

  setCameraLookAt(target) {
    this._cameraLookAt.copy(target);
  }

  /**
   * 更新六个箭头mesh的位置
   * @param offsetX x轴的两个箭头应该相对中心，在模型x轴上的偏移距离，始终为正数
   * @param offsetY y轴的两个箭头应该相对中心，在模型y轴上的偏移距离，始终为正数
   * @param offsetZ z轴的两个箭头应该相对中心，在模型z轴上的偏移距离，始终为正数
   * @private
   */
  _updateArrowOffset(offsetX, offsetY, offsetZ) {
    this._arrowX.position.x = offsetX;
    this._arrowXMinus.position.x = -offsetX;
    this._arrowY.position.y = offsetY;
    this._arrowYMinus.position.y = -offsetY;
    this._arrowZ.position.z = offsetZ;
    this._arrowZMinus.position.z = -offsetZ;
    // const x = this.position.x + this._arrowX.position.x
    // console.log('global x: ', x)
  }

  /**
   * 拖动开始时，设置撞击平面和缩放的投影轴线
   * @private
   */
  _selectHitTestPlaneAndProjectLine() {
    this._hitPlane = this._planeXY;
    scalingOnLine = lineX;
    let planes = [];
    switch (this.axis) {
      case AXES.X:
      case AXES.X_:
        planes = [this._planeXZ, this._planeXY];
        scalingOnLine = lineX;
        this._halfLength = this._objSize.x * 0.5;
        this._halfScaledLength = Math.abs(
          this._objSize.x * this.object.scale.x * 0.5,
        );
        break;
      case AXES.Y:
      case AXES.Y_:
        planes = [this._planeXY, this._planeYZ];
        scalingOnLine = lineY;
        this._halfLength = this._objSize.y * 0.5;
        this._halfScaledLength = Math.abs(
          this._objSize.y * this.object.scale.y * 0.5,
        );
        break;
      case AXES.Z:
      case AXES.Z_:
        planes = [this._planeXZ, this._planeYZ];
        scalingOnLine = lineZ;
        this._halfLength = this._objSize.z * 0.5;
        this._halfScaledLength = Math.abs(
          this._objSize.z * this.object.scale.z * 0.5,
        );
        break;
      default:
        return;
    }
    const [dot0, dot1] = planes.map((plane) =>
      Math.abs(plane.userData.normal.dot(this.eye)),
    );
    // 数量积越大，和相机的视线角度越小
    if (dot0 > dot1) {
      this._hitPlane = planes[0];
    } else {
      this._hitPlane = planes[1];
    }
  }

  // 鼠标悬停在 子mesh上，高亮此子mesh
  _highlightHandler(event) {
    const pointer = canvasCoordToScreenCoord(
      {
        x: event.clientX,
        y: event.clientY,
      },
      event.target.getBoundingClientRect(),
    );
    _raycaster.setFromCamera(pointer, this.camera);
    if (!this.dragging) {
      this.handlers.forEach((mesh) => (mesh.material.opacity = 0.4));
      const intersects = _raycaster.intersectObjects(this.handlers, false);
      if (!intersects.length) {
        return;
      }
      intersects[0].object.material.opacity = 1.0;
    }
  }

  // 使六个缩放箭头始终朝向相机
  updateMatrixWorld(force) {
    if (!this.object) {
      return;
    }
    this.position.setFromMatrixPosition(this.object.matrixWorld);
    this.cameraPosition.copy(this.camera.position);
    const eye = this.eye;
    // eye.copy(this.cameraPosition).sub(this.object.position).normalize()
    this.cameraPosition.applyMatrix4(objRotateInvertMatrix);
    tmpPosition.copy(this.position).applyMatrix4(objRotateInvertMatrix);
    // 把eye向量 从scene坐标系 转换为 物体本地坐标系的方向向量
    eye.copy(this.cameraPosition).sub(tmpPosition).normalize();
    this.arrows.forEach((arrow) => {
      if (arrow.name.search('X') !== -1) {
        vector2.set(eye.y, eye.z).normalize();
        const angle = getOldToNewAngle(normalOnPlane, vector2);
        arrow.rotation.x = angle;
      } else if (arrow.name.search('Y') !== -1) {
        vector2.set(eye.x, eye.z).normalize();
        const angle = -getOldToNewAngle(normalOnPlane, vector2);
        arrow.rotation.y = angle;
      } else {
        vector2.set(eye.x, eye.y).normalize();
        const angle = getOldToNewAngle(normalOnPlane, vector2);
        arrow.rotation.z = angle;
      }
    });

    let factor;
    if (this.camera.isOrthographicCamera) {
      factor = (this.camera.top - this.camera.bottom) / this.camera.zoom;
    } else {
      // _modelViewMatrix.multiplyMatrices(this.camera.matrixWorldInverse, this.object.matrixWorld)
      _modelMatrix.compose(
        this._cameraLookAt,
        this.object.quaternion,
        this.object.scale,
      );
      _modelViewMatrix.multiplyMatrices(
        this.camera.matrixWorldInverse,
        _modelMatrix,
      );
      _mvPosition.setFromMatrixPosition(_modelViewMatrix);
      factor = (-_mvPosition.z * this.camera.fov) / 120;
    }
    this.handlers.forEach((mesh) =>
      mesh.scale.set(1, 1, 1).multiplyScalar(factor * 0.25),
    );
    super.updateMatrixWorld(force);
  }

  /**
   *
   * @param {THREE.Object3D} object
   * @return {ScaleControl}
   */
  attach(object) {
    this.object = object;
    this.visible = true;
    this.object.updateMatrixWorld();
    const matrix = this.object.matrixWorld;
    this.position.setFromMatrixPosition(matrix);
    this._cameraLookAt.copy(this.position);
    const rMat = new THREE.Matrix4().extractRotation(matrix);
    const quat = new THREE.Quaternion().setFromRotationMatrix(rMat);
    this.quaternion.copy(quat);
    const objParentToSceneQuat = quat
      .clone()
      .multiply(this.object.quaternion.clone().invert());
    const objSize = getObject3dSize(
      object,
      objParentToSceneQuat,
      this._objSize,
    ).clone();
    initSign(object.scale);
    objSize.multiplyScalar(0.5).multiply(scaleVector);
    this._updateArrowOffset(objSize.x, objSize.y, objSize.z);
    initProjectionLines(object.matrixWorld);

    objRotateMatrix.makeRotationFromQuaternion(this.quaternion);
    objRotateInvertMatrix.copy(objRotateMatrix).invert();
    this._objParentInvMat.copy(this.object.parent.matrixWorld).invert();
    return this;
  }

  detach() {
    this.object = undefined;
    this.visible = false;
    this.axis = AXES.NONE;
    return this;
  }
}

function onDragStart(event) {
  if (!this.object) {
    return;
  }
  const object = this.object;
  getMouseRaycaster(
    _raycaster,
    this.camera,
    event.clientX,
    event.clientY,
    event.target.getBoundingClientRect(),
  );
  const intersects = _raycaster.intersectObjects(this.handlers, false);
  if (!intersects.length) {
    return;
  }
  const intersectObj = intersects[0].object;
  this.axis = intersectObj.name;
  this._selectHitTestPlaneAndProjectLine();
  const planeIntersect = intersectObjectWithRay(
    this._hitPlane,
    _raycaster,
    true,
  );
  if (planeIntersect) {
    this._dragStart.copy(planeIntersect.point);
    // getScaleAnchorVector(scalingOnLine, this._dragStart, new THREE.Box3().setFromObject(object), this._scaleAnchor)
    this._tStart = scalingOnLine.closestPointToPointParameter(
      this._dragStart,
      false,
    );
    scalingOnLine.at(this._tStart, this._lineStart);
    this._positionStart.copy(object.position);
    this.dragging = true;
    eventScaleChanged.value = this.dragging;
    this.dispatchEvent(eventScaleChanged);
  }
}

function onDragging(event) {
  if (!this.object) {
    return;
  }
  this._highlightHandler(event);
  if (!this.dragging) {
    return;
  }

  const planeIntersect = intersectObjectWithRay(
    this._hitPlane,
    _raycaster,
    true,
  );
  if (!planeIntersect) {
    return;
  }
  const worldPosition = planeIntersect.point;
  this._tEnd = scalingOnLine.closestPointToPointParameter(worldPosition, false);
  if (this._tStart === this._tEnd) {
    return;
  }
  scalingOnLine.at(this._tEnd, this._lineEnd);
  // draggingDistance 保存在缩放轴上的拖动距离, 乘以0.5是因为在投影轴上的拖动距离，一半用于位移，一半用于拉伸
  let draggingDistance = this._lineEnd.distanceTo(this._lineStart) * 0.5;
  // sign 为1 放大；为-1缩小
  let sign = 1;
  const draggingNegativeSide =
    this.axis === AXES.X_ || this.axis === AXES.Y_ || this.axis === AXES.Z_;
  if (this._tEnd < this._tStart) {
    sign = -1;
  }
  if (draggingNegativeSide) {
    sign *= -1;
  }
  draggingDistance = draggingDistance * sign;
  const scaleValOld =
    (draggingDistance + this._halfScaledLength) / this._halfLength;
  const scaleVal = Math.max(scaleValOld, minScaleVal);
  if (scaleVal <= minScaleVal) {
    return;
  }
  if (scalingOnLine === lineX) {
    scaleVector.x = scaleVal;
  } else if (scalingOnLine === lineY) {
    scaleVector.y = scaleVal;
  } else {
    scaleVector.z = scaleVal;
  }
  this.object.scale.copy(scaleVector).multiply(signVector);

  tmpVector.copy(this._lineEnd).sub(this._lineStart).multiplyScalar(0.5);
  const moveLen = tmpVector.length();
  tmpVector.transformDirection(this._objParentInvMat).multiplyScalar(moveLen);
  this.object.position.copy(this._positionStart).add(tmpVector);
  this.object.updateMatrixWorld();
  sizeVector.copy(this._objSize);
  sizeVector.multiply(scaleVector).multiplyScalar(0.5);
  this._updateArrowOffset(sizeVector.x, sizeVector.y, sizeVector.z);
}

function onDragEnd() {
  this.dragging = false;
  eventScaleChanged.value = this.dragging;
  this.dispatchEvent(eventScaleChanged);
  // 更新x y z投影轴线，避免连续拉伸不同的轴后，错误地设置this._scaleAnchor；使拉伸吸附功能出问题
  if (this.object) {
    initProjectionLines(this.object.matrixWorld);
  }
}
